/*
 * heatmap.js v2.0.5 | JavaScript Heatmap Library
 *
 * Copyright 2008-2016 Patrick Wied <heatmapjs@patrick-wied.at> - All rights reserved.
 * Dual licensed under MIT and Beerware license
 *
 * :: 2016-09-05 01:16
 */
(function (name, context, factory) {
    // Supports UMD. AMD, CommonJS/Node.js and browser context
    if (typeof module !== "undefined" && module.exports) {
        module.exports = factory();
    } else if (typeof define === "function" && define.amd) {
        define(factory);
    } else {
        context[name] = factory();
    }
})("h337", this, () => {
    // Heatmap Config stores default values and will be merged with instance config
    let HeatmapConfig = {
        defaultRadius: 40,
        defaultRenderer: "canvas2d",
        defaultGradient: {
            0.25: "rgb(0,0,255)",
            0.55: "rgb(0,255,0)",
            0.85: "yellow",
            1.0: "rgb(255,0,0)",
        },
        defaultMaxOpacity: 1,
        defaultMinOpacity: 0,
        defaultBlur: 0.85,
        defaultXField: "x",
        defaultYField: "y",
        defaultValueField: "value",
        plugins: {},
    };
    let Store = (function StoreClosure() {
        let Store = function Store(config) {
            this._coordinator = {};
            this._data = [];
            this._radi = [];
            this._min = 10;
            this._max = 1;
            this._xField = config["xField"] || config.defaultXField;
            this._yField = config["yField"] || config.defaultYField;
            this._valueField = config["valueField"] || config.defaultValueField;

            if (config["radius"]) {
                this._cfgRadius = config["radius"];
            }
        };

        let defaultRadius = HeatmapConfig.defaultRadius;

        Store.prototype = {
            // when forceRender = false -> called from setData, omits renderall event
            _organiseData(dataPoint, forceRender) {
                let x = dataPoint[this._xField];
                let y = dataPoint[this._yField];
                let radi = this._radi;
                let store = this._data;
                let max = this._max;
                let min = this._min;
                let value = dataPoint[this._valueField] || 1;
                let radius = dataPoint.radius || this._cfgRadius || defaultRadius;

                if (!store[x]) {
                    store[x] = [];
                    radi[x] = [];
                }

                if (!store[x][y]) {
                    store[x][y] = value;
                    radi[x][y] = radius;
                } else {
                    store[x][y] += value;
                }
                let storedVal = store[x][y];

                if (storedVal > max) {
                    if (!forceRender) {
                        this._max = storedVal;
                    } else {
                        this.setDataMax(storedVal);
                    }
                    return false;
                } else if (storedVal < min) {
                    if (!forceRender) {
                        this._min = storedVal;
                    } else {
                        this.setDataMin(storedVal);
                    }
                    return false;
                }
                return {
                    x,
                    y,
                    value,
                    radius,
                    min,
                    max,
                };
            },
            _unOrganizeData() {
                let unorganizedData = [];
                let data = this._data;
                let radi = this._radi;

                for (let x in data) {
                    for (let y in data[x]) {
                        unorganizedData.push({
                            x,
                            y,
                            radius: radi[x][y],
                            value: data[x][y],
                        });
                    }
                }
                return {
                    min: this._min,
                    max: this._max,
                    data: unorganizedData,
                };
            },
            _onExtremaChange() {
                this._coordinator.emit("extremachange", {
                    min: this._min,
                    max: this._max,
                });
            },
            addData() {
                if (arguments[0].length > 0) {
                    let dataArr = arguments[0];
                    let dataLen = dataArr.length;
                    while (dataLen--) {
                        this.addData.call(this, dataArr[dataLen]);
                    }
                } else {
                    // add to store
                    let organisedEntry = this._organiseData(arguments[0], true);
                    if (organisedEntry) {
                        // if it's the first datapoint initialize the extremas with it
                        if (this._data.length === 0) {
                            this._min = this._max = organisedEntry.value;
                        }
                        this._coordinator.emit("renderpartial", {
                            min: this._min,
                            max: this._max,
                            data: [organisedEntry],
                        });
                    }
                }
                return this;
            },
            setData(data) {
                let dataPoints = data.data;
                let pointsLen = dataPoints.length;

                // reset data arrays
                this._data = [];
                this._radi = [];

                for (let i = 0; i < pointsLen; i++) {
                    this._organiseData(dataPoints[i], false);
                }
                this._max = data.max;
                this._min = data.min || 0;

                this._onExtremaChange();
                this._coordinator.emit("renderall", this._getInternalData());
                return this;
            },
            removeData() {
                // TODO: implement
            },
            setDataMax(max) {
                this._max = max;
                this._onExtremaChange();
                this._coordinator.emit("renderall", this._getInternalData());
                return this;
            },
            setDataMin(min) {
                this._min = min;
                this._onExtremaChange();
                this._coordinator.emit("renderall", this._getInternalData());
                return this;
            },
            setCoordinator(coordinator) {
                this._coordinator = coordinator;
            },
            _getInternalData() {
                return {
                    max: this._max,
                    min: this._min,
                    data: this._data,
                    radi: this._radi,
                };
            },
            getData() {
                return this._unOrganizeData();
            } /*,

      TODO: rethink.

    getValueAt: function(point) {
      var value;
      var radius = 100;
      var x = point.x;
      var y = point.y;
      var data = this._data;

      if (data[x] && data[x][y]) {
        return data[x][y];
      } else {
        var values = [];
        // radial search for datapoints based on default radius
        for(var distance = 1; distance < radius; distance++) {
          var neighbors = distance * 2 +1;
          var startX = x - distance;
          var startY = y - distance;

          for(var i = 0; i < neighbors; i++) {
            for (var o = 0; o < neighbors; o++) {
              if ((i == 0 || i == neighbors-1) || (o == 0 || o == neighbors-1)) {
                if (data[startY+i] && data[startY+i][startX+o]) {
                  values.push(data[startY+i][startX+o]);
                }
              } else {
                continue;
              }
            }
          }
        }
        if (values.length > 0) {
          return Math.max.apply(Math, values);
        }
      }
      return false;
    }*/,
        };

        return Store;
    })();

    let Canvas2dRenderer = (function Canvas2dRendererClosure() {
        let _getColorPalette = function (config) {
            let gradientConfig = config.gradient || config.defaultGradient;
            let paletteCanvas = document.createElement("canvas");
            let paletteCtx = paletteCanvas.getContext("2d");

            paletteCanvas.width = 256;
            paletteCanvas.height = 1;

            let gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
            for (let key in gradientConfig) {
                gradient.addColorStop(key, gradientConfig[key]);
            }

            paletteCtx.fillStyle = gradient;
            paletteCtx.fillRect(0, 0, 256, 1);

            return paletteCtx.getImageData(0, 0, 256, 1).data;
        };

        let _getPointTemplate = function (radius, blurFactor) {
            let tplCanvas = document.createElement("canvas");
            let tplCtx = tplCanvas.getContext("2d");
            let x = radius;
            let y = radius;
            tplCanvas.width = tplCanvas.height = radius * 2;

            if (blurFactor == 1) {
                tplCtx.beginPath();
                tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
                tplCtx.fillStyle = "rgba(0,0,0,1)";
                tplCtx.fill();
            } else {
                let gradient = tplCtx.createRadialGradient(x, y, radius * blurFactor, x, y, radius);
                gradient.addColorStop(0, "rgba(0,0,0,1)");
                gradient.addColorStop(1, "rgba(0,0,0,0)");
                tplCtx.fillStyle = gradient;
                tplCtx.fillRect(0, 0, 2 * radius, 2 * radius);
            }

            return tplCanvas;
        };

        let _prepareData = function (data) {
            let renderData = [];
            let min = data.min;
            let max = data.max;
            let radi = data.radi;
            var data = data.data;

            let xValues = Object.keys(data);
            let xValuesLen = xValues.length;

            while (xValuesLen--) {
                let xValue = xValues[xValuesLen];
                let yValues = Object.keys(data[xValue]);
                let yValuesLen = yValues.length;
                while (yValuesLen--) {
                    let yValue = yValues[yValuesLen];
                    let value = data[xValue][yValue];
                    let radius = radi[xValue][yValue];
                    renderData.push({
                        x: xValue,
                        y: yValue,
                        value,
                        radius,
                    });
                }
            }

            return {
                min,
                max,
                data: renderData,
            };
        };

        function Canvas2dRenderer(config) {
            let container = config.container;
            let shadowCanvas = (this.shadowCanvas = document.createElement("canvas"));
            let canvas = (this.canvas = config.canvas || document.createElement("canvas"));
            let renderBoundaries = (this._renderBoundaries = [10000, 10000, 0, 0]);

            let computed = getComputedStyle(config.container) || {};

            canvas.className = "heatmap-canvas";

            this._width =
                canvas.width =
                shadowCanvas.width =
                    config.width || +computed.width.replace(/px/, "");
            this._height =
                canvas.height =
                shadowCanvas.height =
                    config.height || +computed.height.replace(/px/, "");

            this.shadowCtx = shadowCanvas.getContext("2d");
            this.ctx = canvas.getContext("2d");

            // @TODO:
            // conditional wrapper

            canvas.style.cssText = shadowCanvas.style.cssText = "position:absolute;left:0;top:0;";

            container.style.position = "relative";
            container.appendChild(canvas);

            this._palette = _getColorPalette(config);
            this._templates = {};

            this._setStyles(config);
        }

        Canvas2dRenderer.prototype = {
            renderPartial(data) {
                if (data.data.length > 0) {
                    this._drawAlpha(data);
                    this._colorize();
                }
            },
            renderAll(data) {
                // reset render boundaries
                this._clear();
                if (data.data.length > 0) {
                    this._drawAlpha(_prepareData(data));
                    this._colorize();
                }
            },
            _updateGradient(config) {
                this._palette = _getColorPalette(config);
            },
            updateConfig(config) {
                if (config["gradient"]) {
                    this._updateGradient(config);
                }
                this._setStyles(config);
            },
            setDimensions(width, height) {
                this._width = width;
                this._height = height;
                this.canvas.width = this.shadowCanvas.width = width;
                this.canvas.height = this.shadowCanvas.height = height;
            },
            _clear() {
                this.shadowCtx.clearRect(0, 0, this._width, this._height);
                this.ctx.clearRect(0, 0, this._width, this._height);
            },
            _setStyles(config) {
                this._blur = config.blur == 0 ? 0 : config.blur || config.defaultBlur;

                if (config.backgroundColor) {
                    this.canvas.style.backgroundColor = config.backgroundColor;
                }

                this._width =
                    this.canvas.width =
                    this.shadowCanvas.width =
                        config.width || this._width;
                this._height =
                    this.canvas.height =
                    this.shadowCanvas.height =
                        config.height || this._height;

                this._opacity = (config.opacity || 0) * 255;
                this._maxOpacity = (config.maxOpacity || config.defaultMaxOpacity) * 255;
                this._minOpacity = (config.minOpacity || config.defaultMinOpacity) * 255;
                this._useGradientOpacity = !!config.useGradientOpacity;
            },
            _drawAlpha(data) {
                let min = (this._min = data.min);
                let max = (this._max = data.max);
                var data = data.data || [];
                let dataLen = data.length;
                // on a point basis?
                let blur = 1 - this._blur;

                while (dataLen--) {
                    let point = data[dataLen];

                    let x = point.x;
                    let y = point.y;
                    let radius = point.radius;
                    // if value is bigger than max
                    // use max as value
                    let value = Math.min(point.value, max);
                    let rectX = x - radius;
                    let rectY = y - radius;
                    let shadowCtx = this.shadowCtx;

                    var tpl;
                    if (!this._templates[radius]) {
                        this._templates[radius] = tpl = _getPointTemplate(radius, blur);
                    } else {
                        tpl = this._templates[radius];
                    }
                    // value from minimum / value range
                    // => [0, 1]
                    let templateAlpha = (value - min) / (max - min);
                    // this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
                    shadowCtx.globalAlpha = templateAlpha < 0.01 ? 0.01 : templateAlpha;

                    shadowCtx.drawImage(tpl, rectX, rectY);

                    // update renderBoundaries
                    if (rectX < this._renderBoundaries[0]) {
                        this._renderBoundaries[0] = rectX;
                    }
                    if (rectY < this._renderBoundaries[1]) {
                        this._renderBoundaries[1] = rectY;
                    }
                    if (rectX + 2 * radius > this._renderBoundaries[2]) {
                        this._renderBoundaries[2] = rectX + 2 * radius;
                    }
                    if (rectY + 2 * radius > this._renderBoundaries[3]) {
                        this._renderBoundaries[3] = rectY + 2 * radius;
                    }
                }
            },
            _colorize() {
                let x = this._renderBoundaries[0];
                let y = this._renderBoundaries[1];
                let width = this._renderBoundaries[2] - x;
                let height = this._renderBoundaries[3] - y;
                let maxWidth = this._width;
                let maxHeight = this._height;
                let opacity = this._opacity;
                let maxOpacity = this._maxOpacity;
                let minOpacity = this._minOpacity;
                let useGradientOpacity = this._useGradientOpacity;

                if (x < 0) {
                    x = 0;
                }
                if (y < 0) {
                    y = 0;
                }
                if (x + width > maxWidth) {
                    width = maxWidth - x;
                }
                if (y + height > maxHeight) {
                    height = maxHeight - y;
                }

                let img = this.shadowCtx.getImageData(x, y, width, height);
                let imgData = img.data;
                let len = imgData.length;
                let palette = this._palette;

                for (let i = 3; i < len; i += 4) {
                    let alpha = imgData[i];
                    let offset = alpha * 4;

                    if (!offset) {
                        continue;
                    }

                    var finalAlpha;
                    if (opacity > 0) {
                        finalAlpha = opacity;
                    } else if (alpha < maxOpacity) {
                        if (alpha < minOpacity) {
                            finalAlpha = minOpacity;
                        } else {
                            finalAlpha = alpha;
                        }
                    } else {
                        finalAlpha = maxOpacity;
                    }

                    imgData[i - 3] = palette[offset];
                    imgData[i - 2] = palette[offset + 1];
                    imgData[i - 1] = palette[offset + 2];
                    imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha;
                }

                // img.data = imgData;
                this.ctx.putImageData(img, x, y);

                this._renderBoundaries = [1000, 1000, 0, 0];
            },
            getValueAt(point) {
                let value;
                let shadowCtx = this.shadowCtx;
                let img = shadowCtx.getImageData(point.x, point.y, 1, 1);
                let data = img.data[3];
                let max = this._max;
                let min = this._min;

                value = (Math.abs(max - min) * (data / 255)) >> 0;

                return value;
            },
            getDataURL() {
                return this.canvas.toDataURL();
            },
        };

        return Canvas2dRenderer;
    })();

    let Renderer = (function RendererClosure() {
        let rendererFn = false;

        if (HeatmapConfig["defaultRenderer"] === "canvas2d") {
            rendererFn = Canvas2dRenderer;
        }

        return rendererFn;
    })();

    let Util = {
        merge() {
            let merged = {};
            let argsLen = arguments.length;
            for (let i = 0; i < argsLen; i++) {
                let obj = arguments[i];
                for (let key in obj) {
                    merged[key] = obj[key];
                }
            }
            return merged;
        },
    };
    // Heatmap Constructor
    let Heatmap = (function HeatmapClosure() {
        let Coordinator = (function CoordinatorClosure() {
            function Coordinator() {
                this.cStore = {};
            }

            Coordinator.prototype = {
                on(evtName, callback, scope) {
                    let cStore = this.cStore;

                    if (!cStore[evtName]) {
                        cStore[evtName] = [];
                    }
                    cStore[evtName].push((data) => {
                        return callback.call(scope, data);
                    });
                },
                emit(evtName, data) {
                    let cStore = this.cStore;
                    if (cStore[evtName]) {
                        let len = cStore[evtName].length;
                        for (let i = 0; i < len; i++) {
                            let callback = cStore[evtName][i];
                            callback(data);
                        }
                    }
                },
            };

            return Coordinator;
        })();

        let _connect = function (scope) {
            let renderer = scope._renderer;
            let coordinator = scope._coordinator;
            let store = scope._store;

            coordinator.on("renderpartial", renderer.renderPartial, renderer);
            coordinator.on("renderall", renderer.renderAll, renderer);
            coordinator.on("extremachange", (data) => {
                scope._config.onExtremaChange &&
                    scope._config.onExtremaChange({
                        min: data.min,
                        max: data.max,
                        gradient: scope._config["gradient"] || scope._config["defaultGradient"],
                    });
            });
            store.setCoordinator(coordinator);
        };

        function Heatmap() {
            let config = (this._config = Util.merge(HeatmapConfig, arguments[0] || {}));
            this._coordinator = new Coordinator();
            if (config["plugin"]) {
                let pluginToLoad = config["plugin"];
                if (!HeatmapConfig.plugins[pluginToLoad]) {
                    throw new Error(
                        `Plugin '${pluginToLoad}' not found. Maybe it was not registered.`,
                    );
                } else {
                    let plugin = HeatmapConfig.plugins[pluginToLoad];
                    // set plugin renderer and store
                    this._renderer = new plugin.renderer(config);
                    this._store = new plugin.store(config);
                }
            } else {
                this._renderer = new Renderer(config);
                this._store = new Store(config);
            }
            _connect(this);
        }

        // @TODO:
        // add API documentation
        Heatmap.prototype = {
            addData() {
                this._store.addData.apply(this._store, arguments);
                return this;
            },
            removeData() {
                this._store.removeData && this._store.removeData.apply(this._store, arguments);
                return this;
            },
            setData() {
                this._store.setData.apply(this._store, arguments);
                return this;
            },
            setDataMax() {
                this._store.setDataMax.apply(this._store, arguments);
                return this;
            },
            setDataMin() {
                this._store.setDataMin.apply(this._store, arguments);
                return this;
            },
            configure(config) {
                this._config = Util.merge(this._config, config);
                this._renderer.updateConfig(this._config);
                this._coordinator.emit("renderall", this._store._getInternalData());
                return this;
            },
            repaint() {
                this._coordinator.emit("renderall", this._store._getInternalData());
                return this;
            },
            getData() {
                return this._store.getData();
            },
            getDataURL() {
                return this._renderer.getDataURL();
            },
            getValueAt(point) {
                if (this._store.getValueAt) {
                    return this._store.getValueAt(point);
                } else if (this._renderer.getValueAt) {
                    return this._renderer.getValueAt(point);
                }
                return null;
            },
        };

        return Heatmap;
    })();

    // core
    let heatmapFactory = {
        create(config) {
            return new Heatmap(config);
        },
        register(pluginKey, plugin) {
            HeatmapConfig.plugins[pluginKey] = plugin;
        },
    };

    return heatmapFactory;
});
